//***************************************************************************************
// Simple first person style camera class that lets the viewer explore the 3D scene.
//   -It keeps track of the camera coordinate system relative to the world space
//    so that the view matrix can be constructed.  
//   -It keeps track of the viewing frustum of the camera so that the projection
//    matrix can be obtained.
//***************************************************************************************

#include "Camera.h"

namespace D3D10Utils
{
    Camera::Camera()
	    : mPosition(0.0f, 0.0f, 0.0f), 
	      mRight(1.0f, 0.0f, 0.0f),
	      mUp(0.0f, 1.0f, 0.0f),
	      mLook(0.0f, 0.0f, 1.0f)
    {
	    setLens(0.25f * DirectX::XM_PI, 1.0f, 1.0f, 1000.0f);
    }

    Camera::~Camera()
    {
    }

    void Camera::setLens(const float fovY, const float aspect, const float nearPlaneZ, const float farPlaneZ)
    {
	    // cache properties
	    mFovY = fovY;
	    mAspect = aspect;
	    mNearZ = nearPlaneZ;
	    mFarZ = farPlaneZ;

	    mNearWindowHeight = 2.0f * mNearZ * tanf( 0.5f * mFovY );
	    mFarWindowHeight  = 2.0f * mFarZ * tanf( 0.5f * mFovY );

	    const DirectX::XMMATRIX projection = DirectX::XMMatrixPerspectiveFovLH(mFovY, mAspect, mNearZ, mFarZ);
	    DirectX::XMStoreFloat4x4A(&mProjection, projection);
    }

    void Camera::lookAt(DirectX::FXMVECTOR pos, DirectX::FXMVECTOR target, DirectX::FXMVECTOR worldUp)
    {
	    const DirectX::XMVECTOR look = DirectX::XMVector3Normalize(DirectX::XMVectorSubtract(target, pos));
	    const DirectX::XMVECTOR right = DirectX::XMVector3Normalize(DirectX::XMVector3Cross(worldUp, look));
	    const DirectX::XMVECTOR up = DirectX::XMVector3Cross(look, right);

	    DirectX::XMStoreFloat3A(&mPosition, pos);
	    DirectX::XMStoreFloat3A(&mLook, look);
	    DirectX::XMStoreFloat3A(&mRight, right);
	    DirectX::XMStoreFloat3A(&mUp, up);
    }

    void Camera::lookAt(const DirectX::XMFLOAT3A& pos, const DirectX::XMFLOAT3A& target, const DirectX::XMFLOAT3A& up)
    {
	    const DirectX::XMVECTOR positionVector = DirectX::XMLoadFloat3A(&pos);
	    const DirectX::XMVECTOR targetVector = DirectX::XMLoadFloat3A(&target);
	    const DirectX::XMVECTOR upVector = DirectX::XMLoadFloat3A(&up);

	    lookAt(positionVector, targetVector, upVector);
    }

    void Camera::strafe(const float d)
    {
	    // mPosition += d * mRight
	    const DirectX::XMVECTOR replication = DirectX::XMVectorReplicate(d);
	    const DirectX::XMVECTOR right = DirectX::XMLoadFloat3A(&mRight);
	    const DirectX::XMVECTOR position = DirectX::XMLoadFloat3A(&mPosition);
	    DirectX::XMStoreFloat3A(&mPosition, DirectX::XMVectorMultiplyAdd(replication, right, position));
    }

    void Camera::walk(const float d)
    {
	    // mPosition += d * mLook
	    const DirectX::XMVECTOR replication = DirectX::XMVectorReplicate(d);
	    const DirectX::XMVECTOR look = DirectX::XMLoadFloat3A(&mLook);
	    const DirectX::XMVECTOR position = DirectX::XMLoadFloat3A(&mPosition);
	    DirectX::XMStoreFloat3A(&mPosition, DirectX::XMVectorMultiplyAdd(replication, look, position));
    }

    void Camera::pitch(const float angle)
    {
	    // Rotate up and look vector about the right vector.
	    const DirectX::XMMATRIX rotation = DirectX::XMMatrixRotationAxis(DirectX::XMLoadFloat3A(&mRight), angle);

	    DirectX::XMStoreFloat3A(&mUp, DirectX::XMVector3TransformNormal(DirectX::XMLoadFloat3A(&mUp), rotation));
	    DirectX::XMStoreFloat3A(&mLook, DirectX::XMVector3TransformNormal(DirectX::XMLoadFloat3A(&mLook), rotation));
    }

    void Camera::yaw(const float angle)
    {
	    // Rotate the basis vectors about the world y-axis.
	    const DirectX::XMMATRIX rotation = DirectX::XMMatrixRotationY(angle);

	    DirectX::XMStoreFloat3A(&mRight, DirectX::XMVector3TransformNormal(DirectX::XMLoadFloat3A(&mRight), rotation));
	    DirectX::XMStoreFloat3A(&mUp, DirectX::XMVector3TransformNormal(DirectX::XMLoadFloat3A(&mUp), rotation));
	    DirectX::XMStoreFloat3A(&mLook, DirectX::XMVector3TransformNormal(DirectX::XMLoadFloat3A(&mLook), rotation));
    }

    void Camera::updateViewMatrix()
    {
	    // Keep camera's axes orthogonal to each other and of unit length.
        DirectX::XMVECTOR right = DirectX::XMLoadFloat3A(&mRight);
	    DirectX::XMVECTOR look = DirectX::XMLoadFloat3A(&mLook);
	    look = DirectX::XMVector3Normalize(look);
	    const DirectX::XMVECTOR up = DirectX::XMVector3Normalize(DirectX::XMVector3Cross(look, right));

	    // U, L already ortho-normal, so no need to normalize cross product.
	    right = DirectX::XMVector3Cross(up, look); 

	    // Fill in the view matrix entries.
	    const DirectX::XMVECTOR position = DirectX::XMLoadFloat3A(&mPosition);
	    const float x = -DirectX::XMVectorGetX(DirectX::XMVector3Dot(position, right));
	    const float y = -DirectX::XMVectorGetX(DirectX::XMVector3Dot(position, up));
	    const float z = -DirectX::XMVectorGetX(DirectX::XMVector3Dot(position, look));

	    DirectX::XMStoreFloat3A(&mRight, right);
	    DirectX::XMStoreFloat3A(&mUp, up);
	    DirectX::XMStoreFloat3A(&mLook, look);

	    mView(0,0) = mRight.x; 
	    mView(1,0) = mRight.y; 
	    mView(2,0) = mRight.z; 
	    mView(3,0) = x;

	    mView(0,1) = mUp.x;
	    mView(1,1) = mUp.y;
	    mView(2,1) = mUp.z;
	    mView(3,1) = y;

	    mView(0,2) = mLook.x; 
	    mView(1,2) = mLook.y; 
	    mView(2,2) = mLook.z; 
	    mView(3,2) = z;

	    mView(0,3) = 0.0f;
	    mView(1,3) = 0.0f;
	    mView(2,3) = 0.0f;
	    mView(3,3) = 1.0f;
    }

    Camera& GetCamera()
    {
        static Camera camera;

        return camera;
    }
}
